/* 
Copyright (C) 2004-2011 by Andrew B Rahn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
/**
A USB interface to the Microchip PICkit 1 FLASH Starter Kit
device programmer and breadboard.

Orion Sky Lawlor, olawlor@acm.org, 2003/8/3
  See README for changelog.
*/
#ifndef COCOA_APP
#include <usb.h> /* libusb header */
#endif
#include <unistd.h> /* for geteuid */

#if COCOA_APP
#include <CoreFoundation/CoreFoundation.h>

#include <IOKit/IOKitLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/usb/IOUSBLib.h>

#include <mach/mach.h>
#endif
#include "usb_pickit.h"

/* PICkit USB values */
const static int pickit_vendorID=0x04d8; // Microchip, Inc
const static int pickit_productID=0x0032; // PICkit 1 FLASH starter kit
const static int pickit_configuration=2; /* 1: HID; 2: vendor specific */
const static int pickit_interface=0;
const static int pickit_endpoint=1; /* first endpoint for everything */
const static int pickit_timeout=1000; /* timeout in ms */

/* PICkit always uses 8-byte transfers */
const static int reqLen=8;
typedef unsigned char byte;

void bad(const char *why) {
	fprintf(stderr,"Fatal error> %s\n",why);
	exit(17);
}

/* Stupidity:
  In an excellent example of sacrificing backward compatability
for conformance to some proported "standard", the latest Linux 
kernel USB drivers (uhci-alternate in 2.4.24+, and uhci in 2.6.x)
no longer support low speed bulk mode transfers-- they give
"invalid argument", errno=-22, on any attempt to do a low speed 
bulk write.  Thus, we need interrupt mode transfers, which are 
only available via the CVS version of libusb.

(Thanks to Steven Michalske for diagnosing the true problem here.)
*/
#ifndef HAVE_LIBUSB_INTERRUPT_MODE 
#  define HAVE_LIBUSB_INTERRUPT_MODE 1 
#endif

#if HAVE_LIBUSB_INTERRUPT_MODE
/* latest libusb: interrupt mode, works with all kernels */
#  define PICKIT_USB(direction) usb_interrupt_##direction
#else 
/* older libusb: bulk mode, will only work with older kernels */
#  define PICKIT_USB(direction) usb_bulk_##direction
#endif

/****************** Internal I/O Commands *****************/

#if COCOA_APP

typedef usb_pickit usb_dev_handle;
int PICKIT_USB(write)(usb_pickit *d, int endpoint, char*src, int reqLen, int ms_timeout) {
	return darwin_write( src, reqLen );
}

int PICKIT_USB(read)(usb_pickit *dev, int ep, char *bytes, int size,
							int timeout) {
	return darwin_read( bytes, size );
}
#endif
/** Send this binary string command. */
static void send_usb(usb_pickit *d,const char *src) {
	int r=PICKIT_USB(write)(d,pickit_endpoint,
		(char *)src,reqLen,pickit_timeout);
	if (r!=reqLen) { perror("usb PICkit write"); bad("USB write failed"); }
}

/** Write the next program counter with this word.
    Uses "W" command.
*/
static void send_usb_word(usb_pickit *d,pic14_word w) {
	char cmd[reqLen];
	sprintf(cmd,"W__ZZZZZ");
	cmd[1]=(char)(w&0xff);
	cmd[2]=(char)((w>>8)&0xff);
	send_usb(d,cmd);
}
/** Write the next n program counters with these words. */
static void send_usb_words(usb_pickit *d,unsigned int n,pic14_word *w) {
	unsigned int i;
	for (i=0;i<n;i++) send_usb_word(d,w[i]);
}

/** Read this many bytes from this device */
static void recv_usb(usb_pickit *d,int len,byte *dest) {
	int r=PICKIT_USB(read)(d,pickit_endpoint,dest,len,pickit_timeout);
	if (r!=len) { perror("usb PICkit read"); bad("USB read failed"); }
}

/** Read 4 words from the current address */
static void recv_usb_words4(usb_pickit *d,pic14_word *dest) {
	int i;
	byte recv[reqLen];
	send_usb(d,"RZZZZZZZ");
	recv_usb(d,reqLen,recv);
	for (i=0;i<4;i++) dest[i]=recv[2*i+0]+(recv[2*i+1]<<8);
}

/** Read this many words from the device */
static void recv_usb_words(usb_pickit *d,unsigned int len,pic14_word *dest) {
	while (len>0) {
		pic14_word recv[4];
		unsigned int i, c=4; /* # of words to copy out */
		recv_usb_words4(d,recv);
		if (c>len) c=len;
		for (i=0;i<c;i++) dest[i]=recv[i];
		dest+=c; len-=c;
	}
}

/* debugging: enable debugging error messages in libusb */
extern int usb_debug;

/* Find the first USB device with this vendor and product.
   Exits on errors, like if the device couldn't be found.
*/
usb_pickit *usb_pickit_open(void)
{
#ifndef COCOA_APP
  struct usb_device *device;
  struct usb_bus* bus;

  if (geteuid()!=0)
    bad("This program must be run as root, or made setuid root");
  
#ifdef USB_DEBUG
  usb_debug=4; 
#endif
  printf("Locating USB Microchip(tm) PICkit(tm) (vendor 0x%04x/product 0x%04x)\n",
  	pickit_vendorID,pickit_productID);
  /* (libusb setup code stolen from John Fremlin's cool "usb-robot") */
  usb_init();
  usb_find_busses();
  usb_find_devices();
  
  for (bus=usb_busses;bus!=NULL;bus=bus->next) 
  {
    struct usb_device* usb_devices = bus->devices;
    for(device=usb_devices;device!=NULL;device=device->next)
    {
      if (device->descriptor.idVendor == pickit_vendorID
        &&device->descriptor.idProduct == pickit_productID)
      {
#endif
	  usb_dev_handle *d;
#ifndef COCOA_APP
	  printf( "Found USB PICkit as device '%s' on USB bus %s\n",
		   device->filename,
		   device->bus->dirname);
          d=usb_open(device);
	  if (d)
	  { /* This is our device-- claim it */
#endif
	      byte retData[reqLen];
#ifndef COCOA_APP
	      if (usb_set_configuration(d,pickit_configuration)) {
	          bad("Error setting USB configuration.\n");
	      }
	      if (usb_claim_interface(d,pickit_interface)) {
	          bad("Claim failed-- the USB PICkit is in use by another driver.\n"
		  	"Do a `dmesg` to see which kernel driver has claimed it--\n"
			"You may need to `rmmod hid` or patch your kernel's hid driver.\n");
	      }
	     /* Turn off power to the chip before doing anything.
	         This prevents weird random errors during programming.
	         ( Thanks to Curtis Sell for this fix. )
	      */
#endif
	      usb_pickit_off(d);
	      send_usb(d,"vZZZZZZZ");
	      recv_usb(d,reqLen,retData);
	      printf("Communication established.  Onboard firmware version is %d.%d.%d\n",
		  	(int)retData[0],(int)retData[1],(int)retData[2]);
	      if (retData[0]>0x02u)
	          printf("Warning: USB PICkit major version is %d; last known working version is 2\n",(int)retData[0]);
	      return d;
#ifndef COCOA_APP
	  }
          else 
	      bad("Open failed for USB device");
      }
      /* else some other vendor's device-- keep looking... */
    }
  }
  bad("Could not find USB PICkit device--\n"
      "you might try lsusb to see if it's actually there.");
  return NULL;
#endif
}

/** Turn the device on */
void usb_pickit_on(usb_pickit *d) {
	send_usb(d,"V1ZZZZZZ");
}

/** Turn the device off */
void usb_pickit_off(usb_pickit *d) {
	send_usb(d,"V0ZZZZZZ");
}

/** Fill out this state with the contents of the device. 
 */
void usb_pickit_read(usb_pickit *d,pic14_state *s) {
	usb_pickit_read_program(d,&s->program);
	usb_pickit_read_config(d,&s->config);
}

/** Fill out this program with the current contents of the device. 
 */
void usb_pickit_read_program(usb_pickit *d,pic14_program *p) {
	/* Read EEPROM data */
	int i,nee=0;
	send_usb(d,"PZZZZZZZ");
	while (nee<pic14_ee_len) {
#define eeGlobLen (8*8)
		byte eeData[eeGlobLen];
		send_usb(d,"rrrrrrrr");
		recv_usb(d,eeGlobLen,eeData);
		for (i=0;i<eeGlobLen;i++) 
			p->ee[nee++]=eeData[i];
	}
	send_usb(d,"pZZZZZZZ"); 
	
	/* Read instruction data */
	send_usb(d,"PZZZZZZZ"); 
	recv_usb_words(d,pic14_inst_len,p->inst);
	send_usb(d,"pV1ZZZZZ");
}

/** Send this program to the device.
 */
void usb_pickit_write_program(usb_pickit *d,pic14_program *p) {
	unsigned int i;
	
	/* Write out the EEPROM data */
	send_usb(d,"PZZZZZZZ");
	for (i=0;i<pic14_ee_len;i++) {
		char cmd[reqLen];
		sprintf(cmd,"D_ZZZZZZ");
		cmd[1]=(char)(p->ee[i]);
		send_usb(d,cmd);
	}
	send_usb(d,"pZZZZZZZ");
	
	/* Write out the program data */
	send_usb(d,"PZZZZZZZ");
	send_usb_words(d,pic14_inst_len,p->inst);
	send_usb(d,"pV1ZZZZZ");
}

/** Fill out this configuration with the contents of the device. 
 */
void usb_pickit_read_config(usb_pickit *d,pic14_config *c) {
	/* Read OSCCAL from 0x03ff */
	send_usb(d,"V0V1PI\xff\x03");
	recv_usb_words(d,1,&c->osccal);
	/* Read config. id's from 0x2000 */
	send_usb(d,"pV0V1PCZ");
	recv_usb_words(d,pic14_id_len,c->id);
	/* Read config. word from 0x2007 */
	send_usb(d,"pPCI\x07\x00ZZ");
	recv_usb_words(d,1,&c->config);
	send_usb(d,"pV1ZZZZZ");
}

/** Write the configuration (osccal, id, and config word) 
    to the device.  Writes all the bits in the config. word.
 */
void usb_pickit_write_config(usb_pickit *d,pic14_config *c) {
	/* Write OSCCAL to 0x03ff */
	send_usb(d,"V0V1PI\xff\x03");
	send_usb_word(d,c->osccal);
	/* Write config. id's to 0x2000 */
	send_usb(d,"pV0V1PCZ");
	send_usb_words(d,pic14_id_len,c->id);
	/* Write config. word to 0x2007 */
	send_usb(d,"pPCI\x07\x00ZZ");
	send_usb_word(d,c->config);
	send_usb(d,"pV1ZZZZZ");
}

/** Write this state.  If keepOld (RECOMMENDED), will 
   preserve old osccal and BG bits. */
void usb_pickit_write(usb_pickit *d,pic14_state *s,int keepOld) {
	/* Save old config bits */
	pic14_config oldconfig;
	if (keepOld) usb_pickit_read_config(d,&oldconfig);
	
	usb_pickit_reset(d);
	
	/* Program write is slow, so fix config bits first. */
	if (keepOld) /* Normal case: merge new and old configs */
		usb_pickit_merge_config(d,&oldconfig,&s->config);
	else /* DANGEROUS: blast in new config */
		usb_pickit_write_config(d,&s->config);
	
	/* Write new program */
	usb_pickit_write_program(d,&s->program);
}


/** Do a hard chip reset. You *must* preserve config first. */
void usb_pickit_reset(usb_pickit *d) {
	/* Blank out the device completely */
	send_usb(d,"PCEepZZZ");
}

void usb_pickit_merge_config(usb_pickit *d,
	pic14_config *oldconfig, pic14_config *newconfig)
{
	int i;
	pic14_config merged=*newconfig;
	merged.osccal=oldconfig->osccal;
#define BG_MASK 0x3000 /* mask to extract bandgap bits */
        merged.config=(oldconfig->config&BG_MASK)+(newconfig->config&~BG_MASK);
	usb_pickit_write_config(d,&merged);
}

/** Print the whole configuration set (osccal, id, and config word). */
void usb_pickit_print_config(usb_pickit *d) {
	pic14_word id[8];
	int i;
	send_usb(d,"V0PCZZZZ");
	recv_usb_words(d,8,id);
	send_usb(d,"pV1ZZZZZ");
	for (i=0;i<8;i++)
		printf("Configuration data: [0x%04x]=0x%04x  \n",
			0x2000+i,id[i]);
}
